Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MRG/ENH: apply_inverse_cov #6262

Merged
merged 25 commits into from
Mar 4, 2020
Merged

Conversation

dengemann
Copy link
Member

@dengemann dengemann commented May 2, 2019

Here we go: a candidate for a future apply_inverse_cov function that gets us source power by projecting the data covariance.

Feel free to edit!

cc @larsoner @britta-wstnr @wmvanvliet

Screenshot 2019-04-11 21 31 36

@lgtm-com
Copy link

lgtm-com bot commented May 2, 2019

This pull request introduces 2 alerts when merging 9eced1e into 3cc76b9 - view on LGTM.com

new alerts:

  • 2 for Unused import

Comment posted by LGTM.com

@codecov
Copy link

codecov bot commented Jun 13, 2019

Codecov Report

Merging #6262 into master will decrease coverage by 4.42%.
The diff coverage is 100%.

@@            Coverage Diff             @@
##           master    #6262      +/-   ##
==========================================
- Coverage   89.99%   85.57%   -4.43%     
==========================================
  Files         453      453              
  Lines       82020    82100      +80     
  Branches    13000    13002       +2     
==========================================
- Hits        73812    70254    -3558     
- Misses       5381     9177    +3796     
+ Partials     2827     2669     -158

@wmvanvliet
Copy link
Contributor

wmvanvliet commented Jun 13, 2019

Cool stuff @dengemann!

I've looked into how to combine the XYZ and your original was correct, except that when doing K @ cov @ K.T, the values are already squared, hence the combine_xyz should not square them again. I ended replacing the call to combine_xyz with just sol = sol[0::3] + sol[1::3] + sol[2::3].

Another change I made to the example is to also compute the power for the baseline period. Then, we can plot the power relative to baseline, which looks much cleaner.

fig

@bloyl
Copy link
Contributor

bloyl commented Nov 29, 2019

@larsoner @britta-wstnr @wmvanvliet @dengemann
Apart from restructuring and testing what needs to be done here?

@dengemann
Copy link
Member Author

dengemann commented Nov 29, 2019 via email

@bloyl
Copy link
Contributor

bloyl commented Dec 30, 2019

@larsoner @britta-wstnr @wmvanvliet @dengemann
Any opinions on testing? I could add a simple regression test or is there a 3rd party method/implementation we'd like to test against.

@dengemann
Copy link
Member Author

dengemann commented Dec 30, 2019 via email

@bloyl
Copy link
Contributor

bloyl commented Jan 2, 2020

@larsoner @britta-wstnr @wmvanvliet @dengemann
This is ready for a code review. I can't change the title to [MRG] though.

A couple of potential issues remain and need to be discussed.

  1. I'm only testing with pick_ori='normal' since that has a clear expected relationship to what is returned using apply_inverse_raw. I feel like that gets the coverage and the example provides shows good agreement between methods (LCMV and DICS) so I'm fine with an abbreviated test.
  2. Using method='eLORETA' throws an exception in _assemble_kernel
    if method != "MNE":
    noise_norm = noise_norm[src_sel]
    which is strange since eLoreta seems to be tested in other parts of the code base. I don't know if this is a bug in _assemble_kernel or if eLoreta should not be an option for this method. Someone with knowledge of eLoreta should decide if it should be supported here.
  3. The unit label for the colorbar in the topoplots of one of the example figures are overlaping with tick labels. https://17357-1301584-gh.circle-artifacts.com/0/dev/auto_examples/inverse/plot_mne_cov_power.html#sphx-glr-auto-examples-inverse-plot-mne-cov-power-py. Personally, I would remove the unit label but if there is another suggestion that's fine.
  4. Someone with more familiarity with the MNE inverse filters should check that the docstring (mostly copied from apply_inverse) is correct for apply_inverse_cov.
  5. Updating Whats_new is done. I'll put it in the next Push :)

@bloyl bloyl force-pushed the apply_inverse_cov branch 2 times, most recently from 29418d0 to 330b8d3 Compare February 16, 2020 13:31
@bloyl bloyl changed the title WIP/ENH: apply_inverse_cov MRG/ENH: apply_inverse_cov Feb 16, 2020
@bloyl
Copy link
Contributor

bloyl commented Feb 16, 2020

This is ready for a final review.

Copy link
Member

@agramfort agramfort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bloyl if you could add more explanations to both examples eg by making sure you have titles on stc plot always and that you have a short explanation to explain what this figures show. Thx

doc/changes/latest.inc Show resolved Hide resolved
examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
mne/minimum_norm/inverse.py Outdated Show resolved Hide resolved
mne/minimum_norm/inverse.py Outdated Show resolved Hide resolved
# always combine vector components
if is_free_ori:
logger.info(' Combining the current components...')
sol = sol[0::3] + sol[1::3] + sol[2::3]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens when the user specifies pick_ori='vector'?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I don't understand your question. If pick_ori='vector' then _is_free_ori is True and you combine the outputs from the inverse.

What happens with fixed orientation inverse operators is unfortunately not tested since you can only use pick_ori=None with those and I don't have a comparable call to apply_inverse_raw that will facilitate testing. Presumably, this would be the same as using pick_ori='normal' and a suitably configured inverse.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But pick_ori='vector' should produce a VectorSourceEstimate. We explicitly not support it in the apply_lcmv_cov and apply_dics_cov functions, as it should return vectors pointing in the direction of maximum power and we haven't gotten around to implementing that yet.

mne/minimum_norm/tests/test_inverse.py Outdated Show resolved Hide resolved
mne/minimum_norm/tests/test_inverse.py Outdated Show resolved Hide resolved
@bloyl
Copy link
Contributor

bloyl commented Feb 19, 2020 via email

@dengemann
Copy link
Member Author

Let me know once I should take a look / help.

@larsoner
Copy link
Member

I will say that it would have been helpful to have this api conversation 3 months ago when I took over the pr and asked what restructuring needed to be done.

If you want me to look into the pick_ori='vector' changes I can give it a shot

@bloyl
Copy link
Contributor

bloyl commented Feb 19, 2020

The main thing that needs to be decided is the api. @wmvanvliet has reasonably raised 2 questions:

  1. Should we have a dB : bool transform data to decibels parameter. This was in the original PR and matches some(?) psd code. I prefer not to have data in decibels so I'm fine with either keeping the parameter or never returning decibels. I would say that whatever is decided it should be propagated to the other apply_*_cov methods.

  2. What should we do with pick_ori=vector? I haven't thought too deeply about it but I feel like none of the apply_*_cov methods should return a VectorSourceEstimate since the whole k * cov * K.t formulation comes about from taking the inner product of source waveforms and thus collapsing orientation.
    That being said, I believe that pick_ori=vector is functionally equivalent to pick_ori=None for free orientation inverses. So I have no issue with removing it as an allowed option.

Once those are decided they are minimal changes needed to the method, but the test will need to be reworked depending on the decisions.

@dengemann
Copy link
Member Author

The main thing that needs to be decided is the api. @wmvanvliet has reasonably raised 2 questions:

I think 1 could be ok, although I'm not feeling very strongly about it. 2 is also a point that is somewhat confusing to me, I'd rather not support it at this point. Think that we have a power estimate here!

@larsoner
Copy link
Member

  1. I think we should get rid of the dB transformation and let users do it afterward if they want to.
  2. The easiest path forward is probably to disallow pick_ori='vector' and we can add support for it in a subsequent PR.

I agree with @wmvanvliet that what you get out of pick_ori='vector' should be a VectorSourceEstimate, i.e. the three orientations should not be combined, whereas with pick_ori=None they should be combined (as they are everywhere else in MNE).

@larsoner
Copy link
Member

larsoner commented Mar 2, 2020

Rebased, added support for pick_ori='vector', and simplified the code a bit by routing through apply_inverse itself (DRY).

The only remaining issue I see is the tolerance on the cov calc is not great, but that I think will be solved by #7369, let's try to get that in first, rebase this, and try again.

@larsoner
Copy link
Member

larsoner commented Mar 3, 2020

@wmvanvliet feel free to take a look now, other than the rtol issue that should hopefully be fixed by #7369 we should be good to go.

Comment on lines +1275 to +1278
sol = cov.data[sel][:, sel] @ K.T
sol = np.sum(K * sol.T, axis=1, keepdims=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is not equivalent to
np.diag(K @ cov.data[sel][:, sel] @ K.T)
Which i think is what we want. Doesn't the current code include off-diagonal elements for the src covariance matrix?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is not equivalent to ...

It should be equivalent, though admittedly it is confusing because it looks like it might be computing something different. The diagonal entries of A @ B are given by (A * B.T).sum(axis=1), i.e., the sum of the first-row-of-A-times-first-col-of-B, sum of second-row-of-A-times-second-col-of-B, etc. Just to check it numerically, this code works fine locally if I add it:

    sol = np.sum(K * sol.T, axis=1, keepdims=True)
    sol2 = np.diag(K @ cov.data[sel][:, sel] @ K.T)[:, np.newaxis]
    sol3 = np.sum((K @ cov.data[sel][:, sel]) * K, axis=1, keepdims=True)
    np.testing.assert_allclose(sol, sol2)
    np.testing.assert_allclose(sol, sol3)

Doesn't the current code include off-diagonal elements for the src covariance matrix?

Nope, just the diagonal. The end result is has shape (n_src, 1).

@larsoner
Copy link
Member

larsoner commented Mar 4, 2020

... based on what @wmvanvliet said above:

But pick_ori='vector' should produce a VectorSourceEstimate. We explicitly not support it in the apply_lcmv_cov and apply_dics_cov functions, as it should return vectors pointing in the direction of maximum power and we haven't gotten around to implementing that yet.

I think it makes the most sense just not to allow pick_ori='vector', at least for now. We can always add support for it later.

Good to go for you in that case @bloyl ?

@wmvanvliet
Copy link
Contributor

Going from the CSD to the max-power orientation is computing the largest eigenvector of the matrix. You can see how it is done in the beamformer code when pick_ori='max-power'. I'm fine with just disabling it for now. We can get to it later.

Copy link
Member

@britta-wstnr britta-wstnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am late to the party here, apologies. I added some small comments regarding the examples.

Regarding the almost philosophical discussion about vector solutions and power estimates: to me it makes sense to have one power estimate per source orientation in that case and I have used those before (mostly for checking data/forward models). That's my 2 cents.

examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
examples/inverse/plot_mne_cov_power.py Show resolved Hide resolved
examples/inverse/plot_evoked_ers_source_power.py Outdated Show resolved Hide resolved
examples/inverse/plot_mne_cov_power.py Show resolved Hide resolved
examples/inverse/plot_mne_cov_power.py Show resolved Hide resolved
examples/inverse/plot_mne_cov_power.py Show resolved Hide resolved
mne/minimum_norm/inverse.py Outdated Show resolved Hide resolved
mne/minimum_norm/tests/test_inverse.py Outdated Show resolved Hide resolved
@agramfort
Copy link
Member

agramfort commented Mar 4, 2020 via email

@agramfort
Copy link
Member

agramfort commented Mar 4, 2020 via email

@larsoner larsoner added this to the 0.20 milestone Mar 4, 2020
LCMV beamformer
Linearly constrained minimum variance beamformer, which attempts to
estimate activity for a given source while suppressing cross-talk from
other regions, see :func:`mne.beamformer.make_lcmv`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@britta-wstnr @agramfort thanks for the comments, pushed a commit to address them. In the example I linked to :term:s instead of giving the definitions there, which caused me to add LCMV and DICS to our glossary.rst. @britta-wstnr can you just check to see if this is okay? If it's not, let me know how to modify it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@britta-wstnr I'll merge this once CIs come back happy, but feel free to open a PR to correct the wording

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@larsoner, thanks it is on my list to look at the glossary also with the beamformer tutorial I am writing!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @britta-wstnr if you can get to the tutorial in the next couple of weeks it can make it into 0.20!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do all I can! It has been on my to do list for way too long already anyway 🙈 ... the workload has been high the last couple months.

@larsoner larsoner merged commit c086e76 into mne-tools:master Mar 4, 2020
AdoNunes pushed a commit to AdoNunes/mne-python that referenced this pull request Apr 6, 2020
* mne power example

* Fix combining xyz, compute power relative to baseline

* mv method to min_norm folder

* Fix Doc strings - us dB instead of log

* Add second comparitive example

* Better description

* Ref other example

* WIP: failing test

* Test passes of ori-normal fails for others

* fix example cleanup test

* Fix testing

* rebase

* disallow pick_ori=vector and rework test

* restructure test

* FIX: Marijn comments

* ENH: Simplify by routing through Evoked

* FIX: Fix examples

* FIX: decim

* DOC: Comment [ci skip]

* FIX: Remove cruft

* FIX: Fix after rebase

* FIX: Remove vector

* DOC: Better docs

* DOC: Document another [ci skip]

* FIX: Remove workaround

Co-authored-by: Marijn van Vliet <[email protected]>
Co-authored-by: Luke Bloy <[email protected]>
Co-authored-by: Eric Larson <[email protected]>
AdoNunes pushed a commit to AdoNunes/mne-python that referenced this pull request Apr 6, 2020
* mne power example

* Fix combining xyz, compute power relative to baseline

* mv method to min_norm folder

* Fix Doc strings - us dB instead of log

* Add second comparitive example

* Better description

* Ref other example

* WIP: failing test

* Test passes of ori-normal fails for others

* fix example cleanup test

* Fix testing

* rebase

* disallow pick_ori=vector and rework test

* restructure test

* FIX: Marijn comments

* ENH: Simplify by routing through Evoked

* FIX: Fix examples

* FIX: decim

* DOC: Comment [ci skip]

* FIX: Remove cruft

* FIX: Fix after rebase

* FIX: Remove vector

* DOC: Better docs

* DOC: Document another [ci skip]

* FIX: Remove workaround

Co-authored-by: Marijn van Vliet <[email protected]>
Co-authored-by: Luke Bloy <[email protected]>
Co-authored-by: Eric Larson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants